﻿using System;
using System.Text;
using Dokan;
using System.Collections;
using System.IO;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;
using System.Threading;
using System.Collections.Generic;
using System.Diagnostics;

namespace AzureDriveCore
{
	public class AzureFileManager : DokanOperations
	{
		public static int CacheDurationInSeconds = 10;
		public static int CacheRefreshTime = 5;

		public AzureFileManager(string connectionString)
		{
			_cloudStorageAccount = CloudStorageAccount.Parse(connectionString);

			this._rootController = new AzureRootController(_cloudStorageAccount);
			this._blobController = new AzureBlobController(_cloudStorageAccount);
			this._queueController = new AzureEmptyController(_cloudStorageAccount);
			this._tableController = new AzureEmptyController(_cloudStorageAccount);
			this._emptyController = new AzureEmptyController(_cloudStorageAccount);
		}

		private const string ROOT_DIRECTORY = @"\";
		private CloudStorageAccount _cloudStorageAccount;
		private RequestCacheManager requestCacheManager = new RequestCacheManager();

		private AzureControllerBase _rootController;
		private AzureControllerBase _blobController;
		private AzureControllerBase _queueController;
		private AzureControllerBase _tableController;
		private AzureControllerBase _emptyController;

		#region Empty Methods

		public int CloseFile(string filename, DokanFileInfo info)
		{
			return 0;
		}

		public int FlushFileBuffers(string filename, DokanFileInfo info)
		{
			LogNotImplemented(filename);

			return -1;
		}

		public int LockFile(string filename, long offset, long length, DokanFileInfo info)
		{
			return 0;
		}

		public int MoveFile(string filename, string newname, bool replace, DokanFileInfo info)
		{
			LogNotImplemented(new { filename, newname, replace, });

			return -1;
		}

		public int SetAllocationSize(string filename, long length, DokanFileInfo info)
		{
			LogNotImplemented(new { filename, length, });

			return -1;
		}

		public int UnlockFile(string filename, long offset, long length, DokanFileInfo info)
		{
			return 0;
		}

		public int Unmount(DokanFileInfo info)
		{
			return 0;
		}

		#endregion Empty Methods

		private void UncacheDirectory(string path)
		{
			requestCacheManager.ClearCache(RequestType.FindFiles, path);

			requestCacheManager.ClearCache(RequestType.OpenDirectory, path);

			requestCacheManager.ClearCache(RequestType.FindFiles, Path.GetDirectoryName(path));

			requestCacheManager.ClearCache(RequestType.FindFiles, @"\" + path);

			requestCacheManager.ClearCache(RequestType.OpenDirectory, @"\" + path);

			requestCacheManager.ClearCache(RequestType.FindFiles, Path.GetDirectoryName(@"\" + path));
		}

		private void UncacheFile(string path)
		{
			requestCacheManager.ClearCache(RequestType.CreateFile, path);

			requestCacheManager.ClearCache(RequestType.GetFileInformation, path);

			requestCacheManager.ClearCache(RequestType.FindFiles, Path.GetDirectoryName(path));

			requestCacheManager.ClearCache(RequestType.CreateFile, @"\" + path);

			requestCacheManager.ClearCache(RequestType.GetFileInformation, @"\" + path);

			requestCacheManager.ClearCache(RequestType.FindFiles, Path.GetDirectoryName(@"\" + path));
		}

		private AzureControllerBase GetControllerFromPath(ref string path)
		{
			if (path == null || path.Length == 0 || path == ROOT_DIRECTORY)
			{
				path = "";

				return this._rootController;
			}
			else
			{
				if (path.StartsWith(@"\")) path = path.Substring(1);

				switch (path.Split('/', '\\')[0].ToLower())
				{
					case "blob":
						{
							path = path.Substring(4);
							if (path.StartsWith(@"\")) path = path.Substring(1);
							return this._blobController;
						}

					case "queue":
						{
							path = path.Substring(5);
							if (path.StartsWith(@"\")) path = path.Substring(1);
							return this._queueController;
						}

					case "table":
						{
							path = path.Substring(5);
							if (path.StartsWith(@"\")) path = path.Substring(1);
							return this._tableController;
						}

					default: return this._emptyController;
				}
			}
		}

		public int Cleanup(string filename, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				info.Context = null;

				context.Dispose();
			}

			return 0;
		}

		public int CreateDirectory(string filename, DokanFileInfo info)
		{
			string controllerPath = Path.GetDirectoryName(filename);
			string directoryToCreate = filename.Substring(controllerPath.Length);

			var controller = this.GetControllerFromPath(ref controllerPath);

			if (controller.Type != ControllerType.Empty)
			{
				if (directoryToCreate.StartsWith(@"\")) directoryToCreate = directoryToCreate.Substring(1);

				var context = controller.CreateDirectory(directoryToCreate);

				if (context != null)
				{
					info.IsDirectory = true;

					info.Context = controller.CreateRequest(directoryToCreate);

					this.UncacheDirectory(filename);

					return 0;
				}
				else
				{
					return -DokanNet.ERROR_ACCESS_DENIED;
				}
			}

			LogNotImplemented(filename);

			return -1;
		}

		public int CreateFile(string filename, FileAccess access, FileShare share, FileMode mode, FileOptions options, DokanFileInfo info)
		{
			switch (mode)
			{
				default:
					{
						LogNotImplemented(new { mode, filename, });

						return -1;
					}

				case FileMode.CreateNew:
					{
						var controller = this.GetControllerFromPath(ref filename);

						var context = controller.CreateRequest(filename);
						info.Context = context;

						if (context.CreateFile())
						{
							return 0;
						}

						return -DokanNet.ERROR_FILE_NOT_FOUND;
					}

				case FileMode.Open:
					{
						var result = requestCacheManager.ExecuteOrGetFromCache(AzureFileManager.CacheDurationInSeconds, AzureFileManager.CacheRefreshTime, () =>
						{
							return this.GetControllerFromPath(ref filename)
								.GetFileOrDirectoryResult(filename);
						}, RequestType.CreateFile, filename);

						if (result == null)
						{
							return -DokanNet.ERROR_FILE_NOT_FOUND;
						}

						info.Context = result.FileContext;
						info.IsDirectory = result.IsDirectory;

						return 0;
					}
			}
		}

		public int DeleteDirectory(string filename, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				if (context.DeleteDirectory())
				{
					this.UncacheDirectory(filename);

					return 0;
				}
			}

			LogNotImplemented(filename);

			return -1;
		}

		public int DeleteFile(string filename, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				if (context.DeleteFile())
				{
					this.UncacheFile(filename);

					return 0;
				}
			}

			LogNotImplemented(filename);

			return -1;
		}

		public int FindFiles(string filename, ArrayList files, DokanFileInfo info)
		{
			var result = requestCacheManager.ExecuteOrGetFromCache(AzureFileManager.CacheDurationInSeconds, AzureFileManager.CacheRefreshTime, () =>
			{
				var controller = this.GetControllerFromPath(ref filename);

				return controller.FindFiles(filename);
			}, RequestType.FindFiles, filename);

			if (result != null)
			{
				files.AddRange(result);

				return 0;
			}
			else
			{
				return -1;
			}
		}

		public int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes, ref ulong totalFreeBytes, DokanFileInfo info)
		{
			// Demo Data
			freeBytesAvailable = 512 * 1024 * 1024;

			totalBytes = 1024 * 1024 * 1024;

			totalFreeBytes = 512 * 1024 * 1024;

			return 0;
		}

		public int GetFileInformation(string filename, FileInformation fileinfo, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				fileinfo.Attributes = context.FileAttributes;
				fileinfo.CreationTime = context.CreatedDate;
				fileinfo.LastAccessTime = context.CreatedDate;
				fileinfo.LastWriteTime = context.CreatedDate;
				fileinfo.Length = context.FileSize;

				return 0;
			}

			return -1;
		}

		public int OpenDirectory(string filename, DokanFileInfo info)
		{
			var controller = this.GetControllerFromPath(ref filename);

			var result = controller.GetFileOrDirectoryResult(filename);

			if (result != null && result.IsDirectory == true)
			{
				info.IsDirectory = true;

				info.Context = result.FileContext;

				return 0;
			}
			else
			{
				return -DokanNet.ERROR_PATH_NOT_FOUND;
			}
		}

		public int ReadFile(string filename, byte[] buffer, ref uint readBytes, long offset, DokanFileInfo info)
		{
			try
			{
				var context = info.Context as RequestContextBase;

				if (context != null)
				{
					if (context.FileExists)
					{
						readBytes = context.ReadData(buffer, offset);

						return 0;
					}
				}
				// The code below should not happen... TODO: fix instead of hack!
				////else
				////{
				////    // File context was lost on another thread (this can happen). - Reacquire.
				////    if (filename != null && filename.StartsWith(AzureFileManager.ROOT_DIRECTORY))
				////    {
				////        filename = filename.Substring(AzureFileManager.ROOT_DIRECTORY.Length);
				////    }

				////    info.Context = context = BlobRequestContext.Create(_cloudBlobClient, filename);

				////    readBytes = context.ReadData(buffer, offset);

				////    return 0;
				////}
			}
			catch (Exception ex)
			{
				Console.WriteLine("----------");
				Console.WriteLine("READ ERROR: {0}", ex);
				Console.WriteLine("----------");
			}

			Console.WriteLine("Cannot ReadFile: {0}", filename);

			return -1;
		}

		public int SetEndOfFile(string filename, long length, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				requestCacheManager.ClearCache(RequestType.CreateFile, filename);

				requestCacheManager.ClearCache(RequestType.GetFileInformation, filename);

				// Clear the parent directory as well so that this item does not show up in the cache.
				requestCacheManager.ClearCache(RequestType.FindFiles, Path.GetDirectoryName(filename));

				return 0;
			}

			LogNotImplemented(new { filename, length, });

			return -1;
		}

		public int SetFileAttributes(string filename, FileAttributes attr, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				return 0;
			}

			LogNotImplemented(new { filename, attr, });

			return -1;
		}

		public int SetFileTime(string filename, DateTime ctime, DateTime atime, DateTime mtime, DokanFileInfo info)
		{
			var context = info.Context as RequestContextBase;

			if (context != null)
			{
				return 0;
			}

			LogNotImplemented(new { filename, ctime, atime, mtime, });

			return -1;
		}

		public int WriteFile(string filename, byte[] buffer, ref uint writtenBytes, long offset, DokanFileInfo info)
		{
			try
			{
				var context = info.Context as RequestContextBase;

				if (context != null)
				{
					writtenBytes = context.WriteData(buffer, offset);

					return 0;
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine("----------");
				Console.WriteLine("WRITE ERROR: {0}", ex);
				Console.WriteLine("----------");
			}

			LogNotImplemented(filename);

			return -1;
		}

		private void LogNotImplemented(object data)
		{
			var trace = new StackTrace();

			Console.WriteLine("{0}: {1}", trace.GetFrame(1).GetMethod().Name, data);
		}
	}
}
